from __future__ import absolute_import
import copy
import logging

import os
import tempfile
import urlparse
import yaml
from cStringIO import StringIO
from os.path import abspath, dirname, join, normpath
from os.path import exists
from tokenize import generate_tokens
from forest.resource import Resources
from urllib2 import Request, urlopen, URLError

from forest.errors import MissingFileError, ConfigurationFileNotFound

logger = logging.getLogger('forest.config')
from forest.parsedurl import ParsedURL

import time

DEFAULT_HEADERS = {
    'X-Architecture':'',
    'X-Release':'',
    # X-MAC
}

from forest.util import get_md5, recursive_dict_update

def request_file(someurl):
    ''' Retrieves the url and returns a Response object.

        Returns

            A response object
    '''

    headers = DEFAULT_HEADERS

    req = Request(someurl, None, headers)
    tries = 3
    response = None

    while tries >= 0:
        try:
            tries -= 1
            response = urlopen(req)

            if response is None and tries == 0:
                print "Failed to connect to: %s!" % (someurl)
                req = Request(raw_input("Please enter an alternate url for %s" % someurl))
                tries = 3
                continue

            if response is not None:
                break

        except URLError, e:
            if hasattr(e, 'reason'):
                print 'We failed to reach a server.'
                print 'Reason: ', e.reason
                print 'URL:', someurl
            elif hasattr(e, 'code'):
                print 'The server couldn\'t fulfill the request.'
                print 'Error code: ', e.code
                print 'URL:', someurl

            print "Waiting 5 seconds before retry. Tries: %s" % tries
            time.sleep(5)
            continue

    # everything is fine
    return response


# Some hacks to get !setting scalars to work automatically when calling str(Setting)
# we set the loader class of configs so that q can retrieve its setting
def create_tree_yaml_loader_class(config):
    ''' return a loader which makes certain variables available to new elements

        DEPRECATED
    '''
    class CustomLoader(yaml.Loader):
        config = None
        def __init__(self, *args, **kwargs):
            super(CustomLoader, self).__init__(*args, **kwargs)

        def get_config(self):
            return self.config

    CustomLoader.config = config

    return CustomLoader



class Config(object):
    _is_remote = False
    _resources = None
    config = {}
    DEFAULT_CONFIG_TYPES = {}

    loader = None

    def __init__(self):
        ''' A configuration object. Can be loaded from files. Used (and updated) throughout the run cycle. '''
        self.url = None
        self._filename_local = None

    def __getitem__(self, key):
        if key == 'install_root'  and key not in self.config:
            # XXX: this is the wrong spot for this
            return abspath('installroot')

        return self.config[key]

    def __setitem__(self, key, value):
        self.config[key] = value

    def __delitem__(self, key):
        del self.config[key]

    def __iter__(self):
        return iter(self.config)

    def __len__(self):
        return len(self.config)

    def __contains__(self, key):
        ''' Used to test if the config contains some configuration value.

            eg. 'configkey' in Config

            Args:
                key: A key to check in the configuration file.

            Returns:
                True if the configuration file contains name. False otherwise.
        '''
        try:
            self.get(key)
            return True
        except AttributeError:
            return False

    def get(self, *args):
        ''' Allow evaluating dotted expressions to reach into the configuration "tree".

            ret = Config.get('dict1.dict2.value1')

            ret will equal value1 inside of dict2 inside of dict1
        '''
        path = args[0]

        has_default = len(args) == 2

        parts = path.split('.')

        if len(parts) == 1:

            name, index = self.translate_key(path)

            if has_default:
                val = self.config.get(name, args[1])
            else:
                if name not in self.config:
                    raise AttributeError("Item %s not found in configuration." % name)

                val = self.config.get(name)

            if index is not None:
                val = val[index]

            return self.process_return_type(name, val)

        curr = self.config

        found = True

        for p in parts:

            name, index = self.translate_key(p)

            if name in curr:
                curr = curr[name]

                if index is not None:
                    curr = curr[index]
            else:
                found = False

        if not found:
            if not has_default:
                raise AttributeError("Item %s not found in configuration." % path)

            return self.process_return_type(path, args[1])

        return self.process_return_type(path, curr)

    def get_loader(self):
        ''' Deprecated. '''
        if self.loader is None:
            self.loader = create_tree_yaml_loader_class(self)

        return self.loader

    def join_install_root(self, *args):
        ''' Used to join paths to the installation root. '''
        pathargs = [self.get_install_root()]
        pathargs += args
        return os.path.join(*pathargs)

    def set_loader(self, loader):
        ''' Deprecated. '''
        self.loader = loader

    def set_url(self, url):
        ''' Sets the ParsedURL representing the origin of this file. '''
        self.url = url

    def get_url():
        ''' Returns the ParsedURL representing the origin of this file. '''
        return self.url

    _root_prefix = "/"
    def set_prefix(self, root_prefix):
        ''' Allows override of this config files real path. '''
        # roots are concated right on. ensure no double slashes
        if root_prefix.endswith('/') and root_prefix != '/':
            root_prefix = root_prefix[:-1]

        self._root_prefix = root_prefix

    def get_prefix(self):
        ''' Allows override of this config files real path. '''
        return self._root_prefix

    _real_path = None
    def _set_real_path(self, real_path):
        ''' Allows override of this config files real path. '''
        self._real_path = real_path

    def _get_real_path(self):
        ''' Returns this config files real path. '''
        if self._real_path is None:
            return

    def _set_is_remote(self, b):
        self._is_remote = b

    def is_remote(self):
        ''' Returns true if this config file is from a remote origin. '''
        return self._is_remote
    def is_install(self):
        return self.config.get('install_mode', False)

    def _set_local_filename(self, fn):
        self._filename_local = fn

    def get_local_filename(self):
        ''' Returns the local filename where this config file can be found. '''
        return self._filename_local

    def load_file(self, path):
        ''' Loads a tree from a file or url.

            Arguments

                path(str): A path or url to a file containing a tree config.
        '''

        parsed = ParsedURL(urlparse.urlparse(path))
        self.set_url(parsed)
        config_url = parsed.geturl()

        if parsed.scheme in ['http', 'https', 'ftp']:
            # copy remote treefiles locally
            self._set_is_remote(True)
            self._set_local_filename(self.request_file_to_temporary(config_url))
        elif parsed.scheme == '' or parsed.scheme == 'file':
            parsed.path = abspath(parsed.path)
            self._set_local_filename(parsed.path)
        else:
            raise Exception("Unsupported config file path %s" % path)

        logger.debug('Loading configuration from %s' % config_url)
        config_file = request_file(config_url)

        if config_file is None:
            raise ConfigurationFileNotFound("Configuration file at %s not found!" % config_url)

        confile_file_data = config_file.read()

        try:
            config = yaml.load(confile_file_data, self.get_loader())
            if config is not None:
                self.config = config 
                
        except yaml.scanner.ScannerError, e:
            raise Exception("Invalid syntax in file %s: %s" % (path, e))

        # included files follow an inheritance pattern
        accum_dict = {}
        all_resources = Resources(self, [])
            
        for file in self.config.get('include', []):
            # read a config file relative to this, relative or remote
            real_url = self.translate_path(file).geturl()
            
            
            logger.log(logging.TRACE, 'Loading included file from %s (%s)', real_url, file)
            fn = self.get_defined_file(file)

            if fn is None:
                raise ConfigurationFileNotFound('Error including file %s relative to %s' % (file, path))

            # pass the loader to the included Config so that !setting scalars refer to this tree
            # TODO: REMOVE SETTING SCALARS IMMEDIATELY!
            included_config = Config()
            included_config.set_loader(self.get_loader())
            included_config.load_file(real_url)

            # resources from included files are merged by ids using Resources.__add__ (TODO: change to Resources.merge())
            all_resources += included_config.get_resources()

            recursive_dict_update(accum_dict, included_config.config)

        # merge this objects config onto the config dictionary
        all_resources += Resources(self, self.get('resources', []))
        recursive_dict_update(accum_dict, self.config)
        #print self.get_resources(), '~~~~~~~~~~~~~~DEFINED RESOURCES~~~~~~~~~~~~~~~~~~'
        self.config = copy.deepcopy(accum_dict)

        # add our resource list over all the included

        self._resources = all_resources

    def get_resources(self):
        ''' Returns all resources defined in the config file (and included files).

            Returns:
               A resources object.
        '''
        if self._resources is None:
            self._resources = Resources(self, self.get('resources', []))

        return self._resources

    def process_return_type(self, name, val):
        ''''''
        if name not in self.DEFAULT_CONFIG_TYPES:
            return val

        configtype = self.DEFAULT_CONFIG_TYPES[name]

        return configtype.input_value(val, self)


    def translate_key(self, path):
        ''' Translates a piece of a path into a name and optionally and index.

            Args:
                path: A string represent a config path. Optionally, the a string
                in form string[index] is accepted.

            Returns:
                A tuple (name, index). Where name is the parsed name and index
                is the parsed index or None.

             TODO: move this out as a static
        '''

        tokens = []
        for token in generate_tokens(StringIO(path.strip()).readline):
            if token[0] == 0:
                continue
            tokens.append(token)

        # first part should be a string
        assert tokens[0][0] == 1

        name = tokens[0][1]

        index = None

        if len(tokens) > 1:
            # second a open brace
            assert tokens[1][0] == 51, "Invalid config reference %s. Missing open brace." % path
            # then a number
            assert tokens[2][0] == 2, "Invalid config reference %s. Missing index." % path
            # finally a close brace
            assert tokens[3][0] == 51, "Invalid config reference %s. Missing close brace" % path

            index = int(tokens[2][1])

        return name, index

    def get_type(self, type, *args):
        ''' NS: ??? '''
        val = self.get(*args)

        if isinstance(val, type):
            return val

        return type(self, val)

    def set(self, key, value):
        ''' Sets a dotted keys value. '''
        parts = key.split('.')

        curr = self.config

        notfound = False
        for p in parts[:-1]:
            if p in curr:
                curr = curr[p]
            else:
                notfound = True

        if notfound:
            raise AttributeError("Item %s not found in configuration." % key)

        curr[parts[-1]] = value

    def serialize(self):
        ''' Returns a serialization of self. '''
        cop = copy.deepcopy(self.config)
        if 'include' in cop:
            del cop['include']
        cop['resources'] = self.get_resources().serialize()
        return yaml.dump(cop, default_flow_style=False, indent=4, width=200)

    def get_filename(self):
        return self.url.path

    def get_dirname(self):
        #logger.debug('in get_dirname, returning self.url.path as {}'.format(self.url.path))
        return dirname(self.url.path)

    def get_defined_file(self, path):
        path, md5 = self._get_defined_file(path)
        return path

    def get_file_and_md5(self, path):
        return self._get_defined_file(path)

    def _get_defined_file(self, path):
        ''' This downloads the file if necessary and returns an absolute path to the downloaded file.

            Returns

                A an absolute path to the file and its md5 hash, hexencoded.
        '''
        #logger.debug('In get_defined_file, w arg path as {}'.format(path))
        #logger.debug('Retrieving file %s', path)

        xlat_path = self.translate_path(path)

        #logger.debug('xlat_path.scheme is {}'.format(xlat_path.scheme))
        if xlat_path.scheme != 'file':
            logger.log(logging.TRACE, 'Requesting remote file %s (original: %s)', xlat_path.geturl(), path)
            # request and write to a temporary file
            response = request_file(xlat_path.geturl())
            if response is None:
                raise MissingFileError('No response from server for file {}'.format(xlat_path.geturl()))
            md5 = response.info().get('Content-MD5', '')
            return self.request_file_to_temporary(response=response), md5
        else:
            if not exists(xlat_path.path):
                raise MissingFileError("File %s does not exist relative to %s" % (xlat_path.path, self.get_local_filename()))

            #logger.debug('get_defined_file will return xlat_path.path {}'.format(xlat_path.path))
            md5 = get_md5(xlat_path.path)
            return xlat_path.path, md5


    def request_file_to_temporary(self, path = None, response = None):
        ''' Requests a url and stores it into a temporary file.

            Returns

                The path of the temporary file.
        '''
        #logger.debug('in request_file_to_temporary w path argument at {}'.format(path))
        assert path or response
        if not response:
            response = request_file(path)

        f = tempfile.NamedTemporaryFile(delete=False)
        name = f.name
        f.write(response.read())
        f.close()

        #logger.debug('Downloaded %s to %s', path, name)

        #logger.debug('request_file_to_temporary will return name {}'.format(name))
        return name

    def translate_path(self, path):
        ''' Fixes up a path so we know how to request it.

            Paths are relative to this configuration file.

            path can be relative to this config file or absolute (remote and local) or can be a url.
        '''
        if not isinstance(path, basestring):
            raise Exception('Path to a file is not a string : {}'.format(path))
        parsed = ParsedURL(urlparse.urlparse(path))

        # handle absolute remotes right away
        if parsed.scheme in ['http', 'https', 'ftp']:
            pass
        elif parsed.scheme == '' or parsed.scheme == "file":
            if path.startswith('/'):
                parsed.path = join(self.get_prefix(), path[1:])
            else:
                parsed.path = normpath(join(self.get_dirname(), path))

            # override the paths hostname if the configuration file came from a remote source
            if self.is_remote():
                parsed.hostname = self.url.hostname
                parsed.port = self.url.port
                parsed.scheme = self.url.scheme
                parsed.username = self.url.username
                parsed.password = self.url.password
        else:
            raise Exception('Dont know how to translate filename %s' % path)

        logger.debug('Translated path %s to %s (relative to: %s)', path, parsed.path, self.get_filename())

        return parsed

    # TODO: move these into a "run context"
    def is_dry_run(self):
        return self.get('dry_run', False) is True

    def set_dry_run(self, b):
        self.config['dry_run'] = b

    def set_install_root(self, path):
        self.config['install_root'] = abspath(normpath(path))

    def get_build_root(self):
        return self.get('build_root')

    def set_build_root(self, path):
        self.config['build_root'] = abspath(normpath(path))

    def add_modified(self, value):
        if not self.config.get('changed_files', None):
            self.config['changed_files'] = set()
        self.config['changed_files'].add(value)

    def get_modified(self):
        return self.config.get('changed_files', set())

    def get_install_root(self):
        if 'install_root' not in self: #.config:
            return abspath('installroot')

        return self.config['install_root']

class KernelOptions(object):
    def __init__(self, discovered):
        self.discovered = discovered

    def get_single(self, key, default=None):
        list = self.discovered.get(key, [])

        if len(list) > 0:
            return list[0]

        return default

    def get_multiple(self, key):
        return self.discovered.get(key, [])

    def has(self, key):
        return key in self.discovered


    def check_bool_param(self, opt, default=None):
        opt = self.get_single(opt, default)


        if opt is True:
            return True
        elif opt is False:
            return False
        elif opt.lower() in ['t', 'true', 'on', 'y', 'yes']:
            return True
        elif opt.lower() in ['f', 'false', 'off', 'n', 'no']:
            return False
        else:
            logger.error('Unknown kernel parameter %s, presuming False instead', opt)
            return False

import shlex
OPT_PREFIX = 'tree.'
OPT_PREFIX_LEN = len(OPT_PREFIX)

def discover_kernel_commandline_opts():
    ''' Discover configuration options from /proc/cmdline
        TODO: move to util
    '''
    opts = shlex.split(open('/proc/cmdline','r').read().strip())

    collected_opts = {}

    for opt in opts:

        first_eq = opt.find('=')

        if first_eq == -1:
            continue

        key = opt[0:first_eq]
        # past the =
        value = opt[first_eq+1:]

        if not key.startswith(OPT_PREFIX):
            continue

        key = key[OPT_PREFIX_LEN:]

        collected_opts.setdefault(key, []).append(value)

    return KernelOptions(collected_opts)
